package org.stars.daostars.core.runtime;

import org.stars.daostars.EntityBean;
import org.stars.daostars.core.DaoMap;
import org.stars.daostars.core.Helper;
import org.stars.daostars.core.PropertyDescriptorEx;
import org.stars.daostars.events.DaoEventHandler;
import org.stars.datastructure.map.CaseInsensitiveHashMap;
import org.stars.util.reflect.Setter;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import org.apache.commons.dbutils.RowProcessor;

/**
 * Processore delle righe recuperate da un resultset.
 * 
 * @author Francesco Benincasa (908099)
 * @author Alex Svetina (908303)
 * @version 1.0.0.23-ago-200614.41.18
 * @param <T>
 *            tipo di entity bean da creare.
 */
public class EntityBeanRowProcessor implements RowProcessor, Serializable {

	/**
	 * nome del dao che sta invocando il processore
	 */
	private String		daoName;

	/**
	 * tipo di eb per il quale stiamo generando la lista.
	 */
	private String		entityName;

	/**
	 * colonne recuperate dalla query.
	 */
	private String[]	columns;

	/**
	 * Getter delle colonne
	 * 
	 * @return array delle colonne.
	 */
	public String[] getColumns() {
		return columns;
	}

	/**
	 * Recupera l'elenco delle colonne di un result set.
	 * 
	 * @param rs
	 *            result set
	 * @return array di nomi delle colonne estratte
	 * @throws SQLException
	 *             in caso di errore
	 */
	private String[] getColumns(ResultSet rs) throws SQLException {

		ResultSetMetaData rmd = rs.getMetaData();
		int cols = rmd.getColumnCount();
		String column[] = new String[cols];
		Arrays.fill(column, "");

		for (int col = 1; col <= cols; col++) {
			String columnName = rmd.getColumnName(col);

			column[col - 1] = columnName;
		}

		return column;
	}

	/**
	 * serial id
	 */
	private static final long	serialVersionUID	= -8062716657327808985L;

	/**
	 * gestore degli eventi
	 */
	protected DaoEventHandler	daoEventHandler;

	protected DaoMap			conversionMap;

	/**
	 * Special array value used by <code>mapColumnsToProperties</code> that
	 * indicates there is no bean property that matches a column from a
	 * <code>ResultSet</code>.
	 */
	protected static final int	PROPERTY_NOT_FOUND	= -1;

	/**
	 * BasicRowProcessor constructor. Bean processing defaults to a
	 * BeanProcessor instance.
	 */
	public EntityBeanRowProcessor() {
	}

	/**
	 * Convert a <code>ResultSet</code> row into an <code>Object[]</code>. This
	 * implementation copies column values into the array in the same order
	 * they're returned from the <code>ResultSet</code>. Array elements will be
	 * set to <code>null</code> if the column was SQL NULL.
	 * 
	 * @see org.apache.commons.dbutils.RowProcessor#toArray(java.sql.ResultSet)
	 */
	@Override
	public Object[] toArray(ResultSet rs) throws SQLException {
		ResultSetMetaData meta = rs.getMetaData();
		int cols = meta.getColumnCount();
		Object[] result = new Object[cols];

		for (int i = 0; i < cols; i++) {
			result[i] = rs.getObject(i + 1);
		}

		return result;
	}

	/**
	 * Convert a <code>ResultSet</code> row into a JavaBean. This implementation
	 * delegates to a BeanProcessor instance.
	 * 
	 * @see org.apache.commons.dbutils.BeanProcessor#toBean(java.sql.ResultSet,
	 *      java.lang.Class)
	 * @see org.apache.commons.dbutils.RowProcessor#toBean(java.sql.ResultSet,
	 *      java.lang.Class)
	 */
	@Override
	public <T> T toBean(ResultSet rs, Class<T> type) throws SQLException {

		PropertyDescriptorEx[] props;
		try {
			//PropertyDescriptor[] props1 = ReflectUtil.propertyDescriptors(type);
			props = Helper.convert(type,conversionMap);
		} catch (Exception e) {
			throw (new SQLException(e.getMessage()));
		}

		ResultSetMetaData rsmd = rs.getMetaData();
		
		int[] columnToProperty = this.mapColumnsToProperties(rsmd, props);

		return this.createBean(rs, type, props, columnToProperty);
	}

	/**
	 * Convert a <code>ResultSet</code> into a <code>List</code> of JavaBeans.
	 * This implementation delegates to a BeanProcessor instance.
	 * 
	 * @see org.apache.commons.dbutils.BeanProcessor#toBeanList(java.sql.ResultSet,
	 *      java.lang.Class)
	 * @see org.apache.commons.dbutils.RowProcessor#toBeanList(java.sql.ResultSet,
	 *      java.lang.Class)
	 */
	@Override
	public <T> List<T> toBeanList(ResultSet rs, Class<T> type) throws SQLException {
		// impostiamo elenco colonne recuperate
		columns = getColumns(rs);
		if (daoEventHandler == null) {
			List<T> results = new ArrayList<T>();

			if (!rs.next()) {
				return results;
			}

			PropertyDescriptorEx[] props;
			try {
				props = Helper.convert(type, conversionMap);
			} catch (Exception e) {
				throw (new SQLException(e.getMessage()));
			}
			ResultSetMetaData rsmd = rs.getMetaData();
			int[] columnToProperty = this.mapColumnsToProperties(rsmd, props);

			T obj;

			do {
				obj = this.createBean(rs, type, props, columnToProperty);

				results.add(obj);

			} while (rs.next());

			return results;
		} else {
			List<T> results = new ArrayList<T>();

			daoEventHandler.onBeginListEvent(daoName, entityName, columns);

			if (!rs.next()) {
				daoEventHandler.onEndListEvent(columns);
				return results;
			}

			PropertyDescriptorEx[] props;
			try {
				//PropertyDescriptor[] props1 = ReflectUtil.propertyDescriptors(type);
				props = Helper.convert(type, conversionMap);
			} catch (Exception e1) {
				throw (new SQLException(e1.getMessage()));
			}
			ResultSetMetaData rsmd = rs.getMetaData();
			int[] columnToProperty = this.mapColumnsToProperties(rsmd, props);

			T obj;

			do {
				obj = this.createBean(rs, type, props, columnToProperty);
												
				results.add(obj);
				
				daoEventHandler.onBeanCreatedEvent((EntityBean) obj);

			} while (rs.next());

			daoEventHandler.onEndListEvent(columns);
			return results;
		}
	}

	/**
	 * Convert a <code>ResultSet</code> row into a <code>Map</code>. This
	 * implementation returns a <code>Map</code> with case insensitive column
	 * names as keys. Calls to <code>map.get("COL")</code> and
	 * <code>map.get("col")</code> return the same value.
	 * 
	 * @see org.apache.commons.dbutils.RowProcessor#toMap(java.sql.ResultSet)
	 */
	@Override
	public Map<String, Object> toMap(ResultSet rs) throws SQLException {
		Map<String, Object> result = new CaseInsensitiveHashMap<Object>();
		ResultSetMetaData rsmd = rs.getMetaData();
		int cols = rsmd.getColumnCount();

		for (int i = 1; i <= cols; i++) {
			result.put(rsmd.getColumnName(i), rs.getObject(i));
		}

		return result;
	}
	
	

	/**
	 * Creates a new object and initializes its fields from the ResultSet.
	 * 
	 * @param rs
	 *            The result set.
	 * @param type
	 *            The bean type (the return type of the object).
	 * @param props
	 *            The property descriptors.
	 * @param columnToProperty
	 *            The column indices in the result set.
	 * @return An initialized object.
	 * @throws SQLException
	 *             If a database error occurs.
	 */ 
	private <T> T createBean(ResultSet rs, Class<T> type, PropertyDescriptorEx[] props, int[] columnToProperty) throws SQLException {

		PropertyDescriptorEx prop;
		T bean;
		Object value;
		
		if (daoEventHandler == null) {
			bean = newInstance(type);

			for (int i = 1; i < columnToProperty.length; i++) {

				if (columnToProperty[i] == PROPERTY_NOT_FOUND) {
					continue;
				}

				prop = props[columnToProperty[i]];
								
				value=prop.processColumn(rs, i);
				callSetter(bean, prop, value);
			}

			return bean;
		} else {
			bean = newInstance(type);
			Class<?> propType;

			try {
				daoEventHandler.onBeginRowEvent(columns);
			} catch (Exception a1) {
			}
			for (int i = 1; i < columnToProperty.length; i++) {

				if (columnToProperty[i] == PROPERTY_NOT_FOUND) {
					// la proprietà non è stata trovata
					continue;
				}

				prop = props[columnToProperty[i]];
								
				propType = prop.getType();
				value=prop.processColumn(rs, i);
				try {
					daoEventHandler.onGetColumnEvent(props[columnToProperty[i]].getName(), value, propType);
				} catch (Exception a1) {
				}

				callSetter(bean, prop, value);
			}
			try {
				daoEventHandler.onEndRowEvent(columns);
			} catch (Exception a1) {
			}

			return bean;
		}
	}

	/**
	 * Calls the setter method on the target object for the given property. If
	 * no setter method exists for the property, this method does nothing.
	 * 
	 * @param target
	 *            The object to set the property on.
	 * @param prop
	 *            The property to set.
	 * @param value
	 *            The value to pass into the setter.
	 * @throws SQLException
	 *             if an error occurs setting the property.
	 */
	private void callSetter(Object target, PropertyDescriptorEx prop, Object value) throws SQLException {
		Setter setter = prop.getSetter();

		if (setter == null) {
			return;
		}

		Class<?> param = prop.getType();
		try {
			// Don't call setter if the value object isn't the right type
			if (this.isCompatibleType(value, param)) {
				setter.invoke(target, value);
			}

		} catch (IllegalArgumentException e) {
			throw new SQLException("Can not set " + prop.getName() + ": " + e.getMessage());

		} catch (IllegalAccessException e) {
			throw new SQLException("Can not set " + prop.getName() + ": " + e.getMessage());

		} catch (InvocationTargetException e) {
			throw new SQLException("Can not set " + prop.getName() + ": " + e.getMessage());
		}
	}

	/**
	 * ResultSet.getKeyObject() returns an Integer object for an INT column. The
	 * setter method for the property might take an Integer or a primitive int.
	 * This method returns true if the value can be successfully passed into the
	 * setter method. Remember, Method.invoke() handles the unwrapping of
	 * Integer into an int.
	 * 
	 * @param value
	 *            The value to be passed into the setter method.
	 * @param type
	 *            The setter's parameter type.
	 * @return boolean True if the value is compatible.
	 */
	private boolean isCompatibleType(Object value, Class<?> type) {
		// Do object check first, then primitives
		return value == null || type.isInstance(value) || type.equals(Integer.TYPE) && Integer.class.isInstance(value) || type.equals(Long.TYPE) && Long.class.isInstance(value)
				|| type.equals(Double.TYPE) && Double.class.isInstance(value) || type.equals(Float.TYPE) && Float.class.isInstance(value) || type.equals(Short.TYPE)
				&& Short.class.isInstance(value) || type.equals(Byte.TYPE) && Byte.class.isInstance(value) || type.equals(Character.TYPE) && Character.class.isInstance(value)
				|| type.equals(Boolean.TYPE) && Boolean.class.isInstance(value);

	}

	/**
	 * Factory method that returns a new instance of the given Class. This is
	 * called at the start of the bean creation process and may be overridden to
	 * provide custom behavior like returning a cached bean instance.
	 * 
	 * @param c
	 *            The Class to create an object from.
	 * @return A newly created object of the Class.
	 * @throws SQLException
	 *             if creation failed.
	 */
	private <T> T newInstance(Class<T> c) throws SQLException {
		try {
			return c.newInstance();

		} catch (InstantiationException e) {
			throw new SQLException("Can not create " + c.getName() + ": " + e.getMessage());

		} catch (IllegalAccessException e) {
			throw new SQLException("Can not create " + c.getName() + ": " + e.getMessage());
		}
	}

	/**
	 * The positions in the returned array represent column numbers. The values
	 * stored at each position represent the index in the
	 * <code>PropertyDescriptor[]</code> for the bean property that matches the
	 * column name. If no bean property was found for a column, the position is
	 * set to <code>PROPERTY_NOT_FOUND</code>.
	 * 
	 * @param rsmd
	 *            The <code>ResultSetMetaData</code> containing column
	 *            information.
	 * 
	 * @param props
	 *            The bean property descriptors.
	 * 
	 * @return An int[] with column index to property index mappings. The 0th
	 *         element is meaningless because JDBC column indexing starts at 1.
	 * 
	 * @throws SQLException
	 *             any SQL error
	 */
	protected int[] mapColumnsToProperties(ResultSetMetaData rsmd, PropertyDescriptorEx[] props) throws SQLException {

		int cols = rsmd.getColumnCount();
		int columnToProperty[] = new int[cols + 1];
		Arrays.fill(columnToProperty, PROPERTY_NOT_FOUND);

		for (int col = 1; col <= cols; col++) {
			String columnName = rsmd.getColumnName(col);
			for (int i = 0; i < props.length; i++) {

				if (columnName.equalsIgnoreCase(props[i].getName())) {
					columnToProperty[col] = i;
					break;
				}
			}
		}

		return columnToProperty;
	}

	/**
	 * Getter dell'attributo onGetEntityEventHandler.
	 * 
	 * @return the onGetEntityEventHandler
	 */
	public DaoEventHandler getDaoEventHandler() {
		return daoEventHandler;
	}

	/**
	 * Setter dell'attributo onGetEntityEventHandler.
	 * 
	 * @param onGetEntityEventHandler
	 *            the onGetEntityEventHandler to set
	 */
	public void setDaoEventHandler(DaoEventHandler onGetEntityEventHandler) {
		this.daoEventHandler = onGetEntityEventHandler;
	}

	/**
	 * Getter dell'attributo daoName
	 * 
	 * @return the daoName
	 * 
	 *         public String getDaoName() { return daoName; }
	 */

	/**
	 * Setter dell'attributo daoName
	 * 
	 * @param daoName
	 *            the daoName to set
	 */
	public void setDaoName(String daoName) {
		this.daoName = daoName;
	}

	/**
	 * Getter dell'attributo entityName
	 * 
	 * @return the entityName
	 * 
	 *         public String getEntityName() { return entityName; }
	 */

	/**
	 * Setter dell'attributo entityName
	 * 
	 * @param entityName
	 *            the entityName to set
	 */
	public void setEntityName(String entityName) {
		this.entityName = entityName;
	}

	public void setConversionMap(DaoMap value) {
		conversionMap = value;

	}

}
